package educrypt.ciphers.hill;

import org.ejml.data.MatrixIterator;
import org.ejml.simple.SimpleMatrix;

import educrypt.commons.util.MathUtil;
import educrypt.exceptions.EduCryptException;
import educrypt.i18n.Messages;

public class HillKeyMatrix {

	/* STATIC METHODS */
	private static SimpleMatrix changeBase(SimpleMatrix matrix, int modularBase) {
		MatrixIterator it = matrix.iterator(true, 0, 0, matrix.numRows() - 1,
				matrix.numCols() - 1);
		while (it.hasNext()) {
			double element = it.next();
			assert MathUtil.isNatural(element);
			it.set(MathUtil.changeBase((int) element, modularBase));
		}
		return matrix;
	}

	private static double[][] arrayToSquareMatrix(int[] data) {
		int rowLength = HillKeyMatrix.getRowLength(data.length);
		double[][] squareMatrix = new double[rowLength][rowLength];
		for (int row = 0; row < rowLength; row++) {
			for (int column = 0; column < rowLength; column++) {
				squareMatrix[row][column] = data[row * rowLength + column];
			}
		}
		return squareMatrix;
	}

	private static int getRowLength(int dataLength) {
		double rowLength = Math.sqrt(dataLength);
		assert MathUtil.isNatural(rowLength);
		return (int) rowLength;
	}

	private static SimpleMatrix vectorToMatrix(int[] vector) {
		SimpleMatrix matrix = new SimpleMatrix(vector.length, 1);
		for (int i = 0; i < vector.length; i++) {
			matrix.set(i, vector[i]);
		}
		return matrix;
	}

	private static int[] matrixToVector(SimpleMatrix matrix) {
		assert matrix.isVector();
		int[] vector = new int[matrix.numRows()];
		for (int i = 0; i < matrix.numRows(); i++) {
			double value = matrix.get(i);
			vector[i] = (int) value;
		}
		return vector;
	}

	/* END STATIC METHODS */

	private SimpleMatrix data;
	private int modularBase;

	public HillKeyMatrix(int[] numericKeyword, int modularBase)
			throws EduCryptException {
		this.data = changeBase(new SimpleMatrix(
				arrayToSquareMatrix(numericKeyword)), modularBase);
		this.modularBase = modularBase;
		if (!isInversible()) {
			throw new EduCryptException(
					Messages.getString("errNonInvertibleMatrix"));
		}
	}

	public HillKeyMatrix(double[][] squareMatrix, int modularBase) {
		this.data = changeBase(new SimpleMatrix(squareMatrix), modularBase);
		this.modularBase = modularBase;
	}

	private HillKeyMatrix(SimpleMatrix matrix, int modularBase) {
		this.data = changeBase(matrix.copy(), modularBase);
		this.modularBase = modularBase;
	}

	public boolean isInversible() {
		int gdc = MathUtil.GCD(this.determinant(), modularBase);
		return gdc == 1;
	}

	public HillKeyMatrix inverse() {
		int inverseDeterminant = this.inverseDeterminant();
		HillKeyMatrix adjugateMatrix = this.adjugate();
		return adjugateMatrix.multiply(inverseDeterminant);
	}

	public int[] getAsVector() {
		int cont = 0;
		int[] vector = new int[this.data.numRows() * this.data.numCols()];
		for (int i = 0; i < this.data.numRows(); i++) {
			for (int j = 0; j < this.data.numCols(); j++) {
				double value = this.data.get(i, j);
				vector[cont] = (int) value;
				cont++;
			}
		}
		return vector;
	}

	private HillKeyMatrix multiply(int multiplier) {
		SimpleMatrix multipliedMatrix = new SimpleMatrix(this.data);
		MatrixIterator it = multipliedMatrix.iterator(true, 0, 0,
				multipliedMatrix.numRows() - 1, multipliedMatrix.numCols() - 1);
		while (it.hasNext()) {
			it.set((it.next() * multiplier));
		}
		return new HillKeyMatrix(multipliedMatrix, this.modularBase);
	}

	private HillKeyMatrix adjugate() {
		HillKeyMatrix adjugateMatrix = this.cofactor().transpose();
		return adjugateMatrix;
	}

	private HillKeyMatrix cofactor() {
		int length = this.data.numRows();
		SimpleMatrix minorMatrix = new SimpleMatrix(length, length);
		for (int rowPos = 0; rowPos < length; rowPos++) {
			for (int columnPos = 0; columnPos < length; columnPos++) {
				int subMatrixDeterminant = this.removeRowAndColumn(rowPos,
						columnPos).determinant();
				double cofactorElement = Math.pow(-1, rowPos + columnPos)
						* subMatrixDeterminant;
				minorMatrix.set(rowPos, columnPos, cofactorElement);
			}
		}
		return new HillKeyMatrix(minorMatrix, this.modularBase);
	}

	private HillKeyMatrix removeRowAndColumn(int row, int column) {
		int length = this.data.numRows();
		SimpleMatrix submatrix = new SimpleMatrix(length - 1, length - 1);
		int rowSubmatrix = 0, rowOriginalMatrix = 0;
		while (rowSubmatrix < length - 1) {
			if (rowSubmatrix == row) {
				rowOriginalMatrix++;
			}
			int columnSubmatrix = 0, columnOriginalMatrix = 0;
			while (columnSubmatrix < length - 1) {
				if (columnSubmatrix == column) {
					columnOriginalMatrix++;
				}
				double originalValue = this.data.get(rowOriginalMatrix,
						columnOriginalMatrix);
				submatrix.set(rowSubmatrix, columnSubmatrix, originalValue);

				columnSubmatrix++;
				columnOriginalMatrix++;
			}
			rowSubmatrix++;
			rowOriginalMatrix++;
		}
		return new HillKeyMatrix(submatrix, this.modularBase);
	}

	private HillKeyMatrix transpose() {
		return new HillKeyMatrix(this.data.transpose(), this.modularBase);
	}

	private int determinant() {
		double determinant = this.data.determinant();
		assert MathUtil.isNatural(determinant);
		return (int) determinant;
	}

	private int inverseDeterminant() {
		assert isInversible();
		int determinant = this.determinant();
		boolean inverseFound = false;
		int inverseCandidate = 0;
		while (inverseCandidate < modularBase && !inverseFound) {
			inverseFound = (determinant * inverseCandidate) % modularBase == 1;
			if (!inverseFound) {
				inverseCandidate++;
			}
		}
		return (int) inverseCandidate;
	}

	public int getRowLength() {
		return this.data.numRows();
	}

	public int getColumnLength() {
		return this.getRowLength();
	}

	public String toString() {
		return data.toString();
	}

	public int[] multiplyByVector(int[] vector) {

		assert vector.length == this.getRowLength();
		SimpleMatrix vectorMatrix = vectorToMatrix(vector);
		SimpleMatrix toChangeBase = this.data.mult(vectorMatrix);
		SimpleMatrix resultMatrix = changeBase(toChangeBase, modularBase);
		return matrixToVector(resultMatrix);
	}

	public HillKeyMatrix multiply(HillKeyMatrix m2) {
		return new HillKeyMatrix(data.mult(m2.data), modularBase);
	}
}
